import os
import re
import time
import json
import requests
from xml.etree import ElementTree
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from datetime import datetime

from api.base import *
from api.folder import *
from common.logger import logger
from common.retcode import RetCode, TransferStatus
from common.types import FileInfo, PathInfo, FolderTree, UpInfo
from common.models import FileList, PathList, TreeList
from common.encrypt import API, UA, SUFFIX_PARAM, calculate_md5_sign, calculate_hmac_sign
from common.utils import get_time, get_file_md5, get_upload_chunks, get_chunk_size, get_file_name, get_relative_folder

class UploadAction(FolderAction):
	def __init__(self, sess_wp: SessionWrapper):
		super(UploadAction, self).__init__(sess_wp)

	def _create_upload_file(self, up_info, transfer_info):
		"""创建上传任务，包含秒传检查，返回状态码、创建信息"""
		infos = tuple()
		session_key = self._sess_wp.get_session_key()
		session_secret = self._sess_wp.get_session_secret()

		ret_msg = ''
		try:
			url = API + f"/createUploadFile.action?{SUFFIX_PARAM}"
			date = get_time()
			headers = {
				"SessionKey": session_key,
				"Sign-Type": "1",
				"User-Agent": UA,
				"Date": date,
				"Signature": calculate_hmac_sign(session_secret, session_key, 'POST', url, date),
				"Accept": "application/json;charset=UTF-8",
				"Content-Type": "application/x-www-form-urlencoded",
			}
			post_data = {
				"parentFolderId": up_info.fid,
				"baseFileId": "",
				"fileName": up_info.name,
				"size": up_info.totalSize,
				"md5": get_file_md5(up_info.path, transfer_info.check),
				"lastWrite": "",
				"localPath": up_info.path,
				"opertype": 1,
				"flag": 1,
				"resumePolicy": 1,
				"isLog": 0
			}
			post_data_json = json.dumps(post_data)
			resp = requests.post(url, headers=headers, data=post_data, verify=False, timeout=10)
			if resp:
				resp = resp.json()
				if resp['res_message'] == "UserDayFlowOverLimited":
					_msg = f"path={up_info.path}, The daily transmission of the current login account has been exhausted!"
					ret_msg = '该账号今天的可传输量已经耗尽'
					code = RetCode.UP_EXHAUSTED_ERROR
				elif resp.get('res_code') == 'InfoSecurityErrorCode':
					_msg = f"path={up_info.path} Illegal file!"
					ret_msg = f'{up_info.path}是无效的文件'
					code = RetCode.UP_ILLEGAL_ERROR
				elif resp.get('uploadFileId'):
					upload_file_id = resp['uploadFileId']
					file_upload_url = resp['fileUploadUrl']
					file_commit_url = resp['fileCommitUrl']
					file_data_exists = resp['fileDataExists']
					node = file_upload_url.split('//')[1].split('.')[0]
					_msg = f"successfully created upload task, node={node}"
					infos = (upload_file_id, file_upload_url, file_commit_url, file_data_exists)
					code = RetCode.SUCCESS
				else:
					_msg = f"unknown response resp={resp}. Please contact the developer!"
					ret_msg = '创建上传文件时发生意外错误'
					code = RetCode.UP_UNKNOWN_ERROR
				logger.debug(f'Upload by client [create]: {_msg}')
			else:
				code = RetCode.NETWORK_ERROR
		except Exception as e:
			code = RetCode.UP_CREATE_ERROR
			ret_msg = '创建上传文件时出错'
			logger.error(f'Upload by client [create]: an error occurred! {e}')
		return code, ret_msg, infos

	def _upload_file_data(self, file_upload_url, upload_file_id, up_info, transfer_info, callback=None):
		"""客户端接口上传文件数据"""
		session_key = self._sess_wp.get_session_key()
		session_secret = self._sess_wp.get_session_secret()

		url = f"{file_upload_url}?{SUFFIX_PARAM}"
		date = get_time()
		headers = {
			"SessionKey": session_key,
			"Edrive-UploadFileId": str(upload_file_id),
			"User-Agent": UA,
			"Date": date,
			"Signature": calculate_hmac_sign(session_secret, session_key, 'PUT', url, date),
			"Accept": "application/json;charset=UTF-8",
			"Content-Type": "application/octet-stream",
			"Edrive-UploadFileRange": f"0-{up_info.totalSize}",
			"ResumePolicy": "1"
		}

		def _post_data_generator(chunks_iter):
			for _, post_data in enumerate(chunks_iter):
				yield post_data

				up_info.nowSize += len(post_data)
				if callback:
					if up_info.nowSize >= up_info.totalSize:
						up_info.status = TransferStatus.SUCCESS
					callback(transfer_info, up_info.nowSize)

		up_info.status = TransferStatus.TRANSFERING

		chunk_size = get_chunk_size(up_info.totalSize)
		with open(up_info.path, 'rb') as f:
			chunks_iter = get_upload_chunks(f, chunk_size)

			# 这里返回一个post_data的迭代器，该生成器在requests.put内部迭代
			# 使用了Chunk-Encoded Requests
			data_iter = _post_data_generator(chunks_iter)
			resp = requests.put(url, data=data_iter, headers=headers, verify=False, timeout=None)
			if resp.text != "":
				node = ElementTree.XML(resp.text)
				if node.tag == "error":
					if node.findtext('code') != 'UploadFileCompeletedError':
						logger.error(
							f"Upload by client [data]: an error occurred while uploading data {node.findtext('code')},{node.findtext('message')}")
						return RetCode.FAILED
			
			logger.debug(f"Upload by client [data]: upload {up_info.path} success!")
			return RetCode.SUCCESS

	def _upload_client_commit(self, file_commit_url, upload_file_id):
		"""客户端接口上传确认"""
		session_key = self._sess_wp.get_session_key()
		session_secret = self._sess_wp.get_session_secret()
		fid = ''

		try:
			url = f"{file_commit_url}?{SUFFIX_PARAM}"
			date = get_time()  # 时间戳
			headers = {
				"SessionKey": session_key,
				"User-Agent": UA,
				"Date": date,
				"Signature": calculate_hmac_sign(session_secret, session_key, 'POST', url, date),
				"Accept": "application/json;charset=UTF-8",
				"Content-Type": "application/x-www-form-urlencoded",
			}
			post_data = {
				"uploadFileId": upload_file_id,
				"opertype": 1,
				"isLog": 0,
				"ResumePolicy": 1
			}

			resp = requests.post(url, data=post_data, headers=headers, verify=False, timeout=10)

			node = ElementTree.XML(resp.text)
			if node.tag != 'error':
				fid = node.findtext('id')
				fname = node.findtext('name')
				time = node.findtext('createDate')
				logger.debug(f"Upload by client [commit]: at[{time}] upload[{fname}], fid[{fid}] success!")
			else:
				logger.error(f'Upload by client [commit]: unknown error {resp.text}')
		except Exception as e:
			logger.error(f'Upload by client [commit]: an error occurred! {e}')
		return fid

	##############################################################################
	def _upload_file_by_client(self, up_info, transfer_info, callback=None):
		"""使用客户端接口上传单文件，支持秒传功能
		"""
		code, msg, infos = self._create_upload_file(up_info, transfer_info)
		if code == RetCode.SUCCESS:
			upload_file_id, file_upload_url, file_commit_url, file_data_exists = infos
			if file_data_exists == 1:  # 数据存在，进入秒传流程
				logger.debug(f"Upload by client: [{up_info.path}] enter the quick_up process...")
				fid = self._upload_client_commit(file_commit_url, upload_file_id)
				if not fid:
					msg = f'秒传文件失败：{up_info.path}'
					code = RetCode.UP_COMMIT_ERROR
				elif callback:
					callback(transfer_info, up_info.totalSize)
			else:  # 上传文件数据
				logger.debug(f"Upload by client: [{up_info.path}] enter the normal upload process...")
				code = self._upload_file_data(file_upload_url, upload_file_id, up_info, transfer_info, callback)
				if code == RetCode.SUCCESS:
					fid = self._upload_client_commit(file_commit_url, upload_file_id)
					if not fid:
						msg = f'上传确认失败：{up_info.path}'
						code = RetCode.UP_COMMIT_ERROR
					elif callback:
							callback(transfer_info, up_info.totalSize)
				elif code == RetCode.PAUSED:
					return code, ''
				else:
					msg = f'上传文件失败：{up_info.path}'
					logger.debug(f"Upload by client: [{up_info.path}] normal upload failed!")

		return code, msg

	def _upload_file_by_web(self, up_info, transfer_info, callback=None):
		"""使用网页接口上传单文件，不支持秒传
		"""
		headers = {'Referer': self._host_url}
		url = self._host_url + "/v2/getUserUploadUrl.action"
		resp = self._get(url, headers=headers)
		if not resp:
			logger.error(f"Upload by web: [{up_info.path}] network error(1)!")
			return RetCode.NETWORK_ERROR, f'上传文件失败：{up_info.path}'

		resp = resp.json()
		if 'uploadUrl' in resp:
			upload_url = "https:" + resp['uploadUrl']
		else:
			logger.error(f"Upload by web: [{up_info.path}] failed to obtain upload node!")
			upload_url = ''

		# self._session.headers["Referer"] = self._host_url  # 放到 headers？
		self._sess_wp.set_header("Referer", self._host_url)

		headers.update({"Host": "cloud.189.cn"})
		url = self._host_url + "/main.action"
		resp = self._get(url, headers=headers)
		if not resp:
			logger.error(f"Upload by web: [{up_info.path}] network error(2)!")
			return RetCode.NETWORK_ERROR, f'上传文件失败：{up_info.path}'
		
		sessionKey = re.findall(r"sessionKey = '(.+?)'", resp.text)[0]

		def _call_back(read_monitor):
			up_info.nowSize = read_monitor.bytes_read
			if up_info.status == TransferStatus.TRANSFERING:
				if callback:
					callback(transfer_info, up_info.nowSize)
			if read_monitor.len == read_monitor.bytes_read:
				up_info.status = TransferStatus.SUCCESS

		msg = ''
		with open(up_info.path, 'rb') as file_:
			post_data = MultipartEncoder({
				"parentId": up_info.fid,
				"fname": up_info.name,
				"sessionKey": sessionKey,
				"albumId": "undefined",
				"opertype": "1",
				"upload_file": (up_info.name, file_, 'application/octet-stream')
			})
			headers = {"Content-Type": post_data.content_type}
			up_info.status = TransferStatus.TRANSFERING  # 上传中

			monitor = MultipartEncoderMonitor(post_data, _call_back)
			result = self._post(upload_url, data=monitor, headers=headers, timeout=None)
			fid = ''
			if result:
				result = result.json()
				if 'id' in result:
					fid = result['id']
					code = RetCode.SUCCESS
				else:
					msg = f'上传文件时网络中断：{up_info.path}'
					code = RetCode.FAILED
					logger.error(f"Upload by web: [{up_info.path}] failed, {result}")
			else:  # 网络异常
				msg = f'上传文件时网络异常：{up_info.path}'
				code = RetCode.NETWORK_ERROR
				logger.error(f"Upload by web: [{up_info.path}] network error(3)!")

			return code, msg

	##############################################################################
	
	def upload_file(self, upload_info, transfer_info, callback=None):
		"""单个文件上传接口
		"""
		transfer_info.totalSize = upload_info.totalSize
		if callback:
			to_continue = callback(transfer_info, 0)
			if not to_continue:
				return RetCode.PAUSED, ''

		if not os.path.isfile(upload_info.path):
			return RetCode.PATH_ERROR, f"[{upload_info.path}]不是有效文件路径"

		session_key = self._sess_wp.get_session_key()
		session_secret = self._sess_wp.get_session_secret()
		session_access_token = self._sess_wp.get_session_access_token()

		if not transfer_info.force and upload_info.exist:
			logger.debug(f"Abandon upload because the file is already exist: {upload_info.path}")
			upload_info.status = RetCode.SUCCESS
			if callback:
				callback(transfer_info, upload_info.totalSize)
			return RetCode.SUCCESS, ''
		elif session_key and session_secret and session_access_token:
			logger.debug(f"Use the client interface to upload files: {upload_info.path}, {upload_info.fid}")
			return self._upload_file_by_client(upload_info, transfer_info, callback)
		else:
			logger.debug(f"Use the web interface to upload files: {upload_info.path}, {upload_info.fid}")
			return self._upload_file_by_web(upload_info, transfer_info, callback)

	def _check_if_file_exist(self, files_info_dict, fid, transfer_info, upload_info):
		files_info = []
		if fid not in files_info_dict:
			if not transfer_info.force:
				files_info, _ = self.get_file_list(fid)
			files_info_dict[fid] = files_info
		files_info = files_info_dict[fid]
		# 判断文件是否存在
		exist = False
		for file_info in files_info:
			if upload_info.name == file_info.name and upload_info.totalSize == file_info.size: # 同名同大小即判断为相同
				exist = True
				break
		return exist

	def upload_dir(self, transfer_info, callback=None):
		"""文件夹上传接口
		"""
		if not os.path.isdir(transfer_info.path):
			return RetCode.PATH_ERROR, f"[{transfer_info.path}]不是有效目录路径"

		logger.debug(f'Upload dir: start parsing {transfer_info.path} structure...')

		dir_dict = {}
		files_info_dict = {}
		
		dir_name = get_file_name(transfer_info.path)
		if transfer_info.mkdir:
			folder_id = ''
			code, folder_id = self.mkdir(transfer_info.fid, dir_name)
			if code != RetCode.SUCCESS:
				transfer_info.status = code
				return code, f'创建目录[{dir_name}]失败'
			dir_dict[dir_name] = folder_id
			# files_info_dict[folder_id] = []
		else:
			dir_dict[dir_name] = parrent_fid

		for home, dirs, files in os.walk(transfer_info.path):
			for _file in files:
				f_path = home + os.sep + _file
				f_rfolder = get_relative_folder(f_path, transfer_info.path)
				file_size = os.path.getsize(f_path)  # Byte
				logger.debug(f"Upload dir: home={home}, f_rfolder={f_rfolder}, f_path={f_path}, _file={_file}, file_size={file_size}")
				
				if f_rfolder not in dir_dict:
					dir_dict[f_rfolder] = ''
				folder_id = dir_dict[f_rfolder]

				upload_info = UpInfo()
				upload_info.name = _file
				upload_info.path = f_path
				upload_info.fid = folder_id
				upload_info.totalSize = file_size
				upload_info.exist = self._check_if_file_exist(files_info_dict, folder_id, transfer_info, upload_info)
				transfer_info.upload_info_list.append(upload_info)

			for _dir in dirs:
				p_rfolder = get_relative_folder(home, transfer_info.path, is_file=False)
				dir_rname = p_rfolder + os.sep + _dir  # 文件夹相对路径

				folder_id = ''
				code, folder_id = self.mkdir(dir_dict[p_rfolder], _dir)
				if code != RetCode.SUCCESS:
					logger.error(
						f"Upload dir: create a folder in the upload sub folder{dir_rname} failed! folder_name={folder_name}, p_rfolder={dir_dict[p_rfolder]}")
					transfer_info.status = code
					return code, f'创建目录[{_dir}]失败'

				logger.debug(f"Upload dir: folder created, home={home}, p_rfolder={p_rfolder}, dir_rname={dir_rname}, folder_id={folder_id}")
				dir_dict[dir_rname] = folder_id
				# files_info_dict[folder_id] = []
		
		for index, upload_info in enumerate(transfer_info.upload_info_list, start=1):
			transfer_info.index = index
			code, msg = self.upload_file(upload_info, transfer_info, callback=callback)
			if code != RetCode.SUCCESS:
				return code, msg

		return RetCode.SUCCESS, ''

	def upload_files(self, transfer_info, callback=None):
		"""多文件上传接口
		"""
		if not os.path.isdir(transfer_info.path):
			return RetCode.PATH_ERROR, f"[{transfer_info.path}]不是有效目录路径"

		logger.debug(f'Upload files: {transfer_info.path} {len(transfer_info._upload_files)} filses')

		files_info_dict = {}
		dir_name = get_file_name(transfer_info.path)
		folder_id = transfer_info.fid
		if transfer_info.mkdir:
			code, folder_id = self.mkdir(transfer_info.fid, dir_name)
			if code != RetCode.SUCCESS:
				return code, f'创建目录[{dir_name}]失败'
			files_info_dict[folder_id] = []

		for f_path in transfer_info._upload_files:
			f_name = get_file_name(f_path)
			f_rfolder = get_relative_folder(f_path, transfer_info.path)
			file_size = os.path.getsize(f_path)  # Byte
			logger.debug(f"Upload file: f_rfolder={f_rfolder}, f_path={f_path}, file_size={file_size}")

			upload_info = UpInfo()
			upload_info.name = f_name
			upload_info.path = f_path
			upload_info.fid = folder_id
			upload_info.totalSize = file_size
			upload_info.exist = self._check_if_file_exist(files_info_dict, folder_id, transfer_info, upload_info)
			transfer_info.upload_info_list.append(upload_info)

		for index, upload_info in enumerate(transfer_info.upload_info_list, start=1):
			transfer_info.index = index
			code, msg = self.upload_file(upload_info, transfer_info, callback=callback)
			if code != RetCode.SUCCESS:
				return code, msg

		return RetCode.SUCCESS, ''